package com.lsh.netty.http.httpFileServer;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;

/**
 * @author ：LiuShihao
 * @date ：Created in 2021/6/2 11:55 上午
 * @desc ：HttpFile文件服务器处理类
 */
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private final String url ;
    public HttpFileServerHandler(String url) {
        this.url  = url;
    }
    /**
     * 当服务器接收到消息时，会自动触发 messageReceived方法
     */
    @Override
    protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        // 5.0.0  request.getDecoderRequest() 已经被弃用
        //对HTTP请求消息的解码结果进行判断，如果解码失败则返回400错误
        if (!request.decoderResult().isSuccess()) {
            System.out.println("解码失败返回400");
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        //对请求方法进行判断，如果不是从浏览器或者表单设置为GET发起的请求（例如POST），则返回405错误
        if (request.method() != HttpMethod.GET) {
            sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
            System.out.println("请求方式不是GET，返回405");
            return;
        }

        //如果URI不合法 返回403错误
        final String uri = request.uri();
        System.out.println("request.uri : " + uri);
        final String path = sanitizeUri(uri);
        System.out.println("path : " + path);
        if (path == null) {
            sendError(ctx, HttpResponseStatus.FORBIDDEN);
            System.out.println("URI不合法 返回403错误");
            return;
        }
        //如果文件不存在或者是系统隐藏文件 则返回404错误
        File file = new File(path);

        if (file.isHidden() || !file.exists()) {
            sendError(ctx, HttpResponseStatus.NOT_FOUND);
            System.out.println("文件不存在或者是系统隐藏文件 则返回404错误");
            return;
        }
        if (file.isDirectory()) {
            if (uri.endsWith("/")) {
                //返回
                sendListing(ctx, file);
            } else {
                //重定向
                sendRedirect(ctx, uri + "/");
            }
            return;
        }
        //判断文件合法性
        if (!file.isFile()) {
            sendError(ctx, HttpResponseStatus.FORBIDDEN);
            return;
        }
        RandomAccessFile randomAccessFile = null;
        try {
            //以只读方式打开文件
            randomAccessFile = new RandomAccessFile(file, "r");
        } catch (FileNotFoundException e) {
            sendError(ctx, HttpResponseStatus.NOT_FOUND);
            return;
        }
        //获取文件的长度构造成功的HTTP应答消息
        long fileLength = randomAccessFile.length();
        DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        HttpHeaderUtil.setContentLength(response, fileLength);
        setContentTypeHeader(response, file);
        //判断是否是keepAlive，如果是就在响应头中设置CONNECTION为keepAlive
        if (HttpHeaderUtil.isKeepAlive(request)) {
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        ctx.write(response);
        ChannelFuture sendFileFuture;
        //通过Netty的ChunkedFile对象直接将文件写入到发送缓冲区中
        sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
        //为sendFileFuture添加监听器，如果发送完成打印发送完成的日志
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {
                System.out.println("传输完成.");

            }
            @Override
            public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception {
                if (total < 0) {
                    System.err.println("传输进度 : " + progress);
                } else {
                    System.err.println("传输进度: " + progress + "/" + total);
                }
            }
        });
        //如果使用chunked编码，最后需要发送一个编码结束的空消息体，将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中，
        //来标示所有的消息体已经发送完成，同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送
        ChannelFuture lastContentFuture = ctx.
                writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        //如果是非keepAlive的，最后一包消息发送完成后，服务端要主动断开连接
        if (!HttpHeaderUtil.isKeepAlive(request)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
        cause.printStackTrace();
        if(ctx.channel().isActive()){
            sendError(ctx,HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }
    private static final Pattern INSECURE_URI=Pattern.compile(".*[<>&\"].*");

    /**
     *
     *
     * @param uri
     * @return
     */
    private String sanitizeUri(String uri){
        //对URL进行解码 解码成功后对URI进行合法性判断 如果URI与允许访问的URI一直或者是其子目录（文件），则检验通过否则返回空
        try {
            uri = URLDecoder.decode(uri,"UTF-8");
        }catch (UnsupportedEncodingException e){
            try {
                uri = URLDecoder.decode(uri,"ISO-8859-1");
            }catch (UnsupportedEncodingException e1){
                throw  new Error();
            }
        }
        //解码成功后对uri进行合法性判断，避免访问无权限的目录
        if(!uri.startsWith("/src/com/exp/netty")){
            System.out.println("uri不是以/src/com/exp/netty开头的,uri不合法！");
            return null;
        }
        if(!uri.startsWith("/")){
            System.out.println("uri不是以/开头的");
            return null;
        }

        //将硬编码的文件路径分隔符替换为本地操作系统的文件路径分割符
        uri = uri.replace('/',File.separatorChar);
        //对新的URI做二次合法性校验，如果校验失败则直接返回空
        if (uri.contains(File.separator+'.')
            || uri.contains('.'+File.separator) || uri.startsWith(".")
            || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()){
            System.out.println("新的URI做二次合法性校验失败则直接返回空");
            return null;
        }
        //最后对文件进行拼接，使用当前运行程序所在的工作目录 + URI 构造绝对路径返回
        System.out.println("最后对文件进行拼接:"+System.getProperty("user.dir") + File.separator + uri);
        return System.getProperty("user.dir") + File.separator + uri;

    }

    private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

    /**
     * 发送目录的链接到客户端浏览器
     * @param ctx
     * @param dir
     */
    private static void sendListing(ChannelHandlerContext ctx,File dir){
            //创建成功的http响应消息
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            //设置消息头的类型是html文件，不要设置为text/plain，客户端会当做文本解析
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
            //构造返回的html页面内容
            StringBuilder buf = new StringBuilder();
            String dirPath = dir.getPath();
            buf.append("<!DOCTYPE html>\r\n");
            buf.append("<html><head><title>");
            buf.append(dirPath);
            buf.append("目录：");
            buf.append("</title></head><body>\r\n");
            buf.append("<h3>");
            buf.append(dirPath).append("目录：");
            buf.append("</h3>\r\n");
            buf.append("<ul>");
            buf.append("<li>链接：<a href=\"../\">..</a></li>\r\n");
            for (File f : dir.listFiles()) {
                if (f.isHidden() || !f.canRead()) {
                    continue;
                }
                String name = f.getName();
                if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
                    continue;
                }
                buf.append("<li>链接：<a href=\"");
                buf.append(name);
                buf.append("\">");
                buf.append(name);
                buf.append("</a></li>\r\n");
            }
            buf.append("</ul></body></html>\r\n");
            System.out.println("buf ：" + buf);
            //分配消息缓冲对象
            ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
            //将缓冲区的内容写入响应对象，并释放缓冲区
            response.content().writeBytes(buffer);
            buffer.release();
            //将响应消息发送到缓冲区并刷新到SocketChannel中
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);

    }

    private static void sendRedirect(ChannelHandlerContext ctx,String newUri){
        FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
        response.headers().set(HttpHeaderNames.LOCATION,newUri);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
        FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,status,
                Unpooled.copiedBuffer("Failure: "+status.toString()+"\r\n",CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private static void setContentTypeHeader(HttpResponse response,File file){
        MimetypesFileTypeMap mimetypesTypeMap=new MimetypesFileTypeMap();
        response.headers().set(HttpHeaderNames.CONTENT_TYPE,mimetypesTypeMap.getContentType(file.getPath()));
    }

}
